package kode.boot.file.api;

import kode.base.core.util.EncodeUtil;
import kode.base.core.util.Validate;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Optional;

/**
 * AbstractFileService
 *
 * @author Stark
 * @date 2019/1/4
 */
@Slf4j
public abstract class AbstractFileService<T extends FileMetadata<ID>, ID> implements FileService<T, ID> {

	@Getter
	@Setter
	private FileMetadataService<T, ID> fileMetadataService;

	// read file
	//-----------------------

	@Override
	public InputStream readFile(ID id) {
		T fileMetadata = fileMetadataService.getById(id).orElse(null);
		Validate.notNull(fileMetadata);
		return readFile(fileMetadata.getLocation());
	}

	@Override
	public InputStream readFile(T meta) {
		return readFile(meta.getLocation());
	}

	@Override
	public void writeToStream(ID id, OutputStream stream) throws IOException {
		writeToStream(id, stream, 0);
	}

	@Override
	public void writeToStream(ID id, OutputStream stream, long skip) throws IOException {
		writeToStream(readFile(id), stream, skip);
	}

	@Override
	public void writeToStream(ID id, OutputStream stream, long skip, long end) throws IOException {
		writeToStream(readFile(id), stream, skip, end);
	}

	@Override
	public void writeToStream(String location, OutputStream stream) throws IOException {
		writeToStream(location, stream, 0);
	}

	@Override
	public void writeToStream(String location, OutputStream stream, long skip) throws IOException {
		writeToStream(readFile(location), stream, skip);
	}

	@Override
	public void writeToStream(String location, OutputStream stream, long skip, long end) throws IOException {
		writeToStream(readFile(location), stream, skip, end);
	}

	private void writeToStream(InputStream inputStream, OutputStream outputStream, long skip) throws IOException {
		try (inputStream) {
			if (skip > 0) {
				long len = inputStream.skip(skip);
				log.info("skip write stream {}, actual {}", skip, len);
			}
			StreamUtils.copy(inputStream, outputStream);
		}
	}

	private void writeToStream(InputStream inputStream, OutputStream outputStream, long skip, long end) throws IOException {
		try (inputStream) {
			long len = 0;
			if (skip > 0) {
				len = inputStream.skip(skip);
				log.info("skip write stream {}, actual {}", skip, len);
			}
			StreamUtils.copyRange(inputStream, outputStream, len, end);
		}
	}


	// save file
	//-----------------------

	@Override
	public T saveFileAndMeta(String base64Str, T fileMeta, FileValidator validator) throws IOException {
		saveFile(base64Str, fileMeta, validator);
		return fileMetadataService.saveOrUpdate(fileMeta);
	}

	@Override
	public T saveFileAndMeta(InputStream inputStream, T fileMeta, FileValidator validator) throws IOException {
		saveFile(inputStream, fileMeta, validator);
		return fileMetadataService.saveOrUpdate(fileMeta);
	}

	@Override
	public T saveFileAndMeta(InputStream inputStream, T fileMeta) throws IOException {
		return saveFileAndMeta(inputStream, fileMeta, null);
	}

	@Override
	public void saveFile(String base64Str, T fileMeta, FileValidator validator) throws IOException {
		Validate.notBlank(base64Str);
		saveFile(base64ToStream(base64Str), fileMeta, validator);
	}

	@Override
	public void saveFile(InputStream inputStream, T fileMeta) throws IOException {
		saveFile(inputStream, fileMeta, null);
	}

	// delete file
	//-----------------------

	@Override
	public boolean deleteFile(ID key) {
		Optional<T> meta = fileMetadataService.getById(key);
		return meta.filter(t -> deleteFile(t.getLocation())).isPresent();
	}

	protected InputStream base64ToStream(String base64Str) {
		byte[] bytes = EncodeUtil.base64Decode(base64Str);
		return new ByteArrayInputStream(bytes);
	}

	/**
	 * get image dimension by input stream
	 *
	 * @param inputStream image input stream
	 * @return image dimension
	 * @throws IOException if i/o error occur
	 */
	protected Dimension getImageDimension(InputStream inputStream) throws IOException {
		Validate.notNull(inputStream, "image input stream must not be null.");

		Dimension dimension = null;
		try (ImageInputStream iis = ImageIO.createImageInputStream(inputStream)) {
			final Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
			if (readers.hasNext()) {
				final ImageReader reader = readers.next();
				try {
					reader.setInput(iis);
					dimension = new Dimension(reader.getWidth(0), reader.getHeight(0));
				} finally {
					reader.dispose();
				}
			}
		}

		return dimension;
	}

}
